﻿namespace Mosquito.Wpf.Y {
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.ComponentModel;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Markup;

    [ContentProperty("VirtualChildren")]
    public class GridRow : FrameworkElement {
        #region Dependency properties
        
        /// <summary>
        ///     The VirtualChildren property.
        /// </summary>
        /// <remarks>Children are added to the parent Grid and kept in this collection.</remarks>
        public static readonly DependencyProperty VirtualChildrenProperty =
                DependencyProperty.Register(
                    "VirtualChildren",
                    typeof(ObservableCollection<UIElement>),
                    typeof(GridRow),
                    new FrameworkPropertyMetadata(null));

        /// <summary>
        ///     The StartColumn property. 
        /// </summary>
        /// <remarks>
        ///     Determines the Grid.Column property of the first child.
        /// </remarks>
        public static readonly DependencyProperty StartColumnProperty =
                DependencyProperty.Register(
                    "StartColumn",
                    typeof(int?),
                    typeof(GridRow),
                    new FrameworkPropertyMetadata(null, OnStartColumnPropertyChanged));

        /// <summary>
        ///     The LayoutMode property.
        /// </summary>
        /// <remarks>
        ///     Determines if the GridRow respects the Grid.Row attached property or not.
        /// </remarks>
        public static readonly DependencyProperty LayoutModeProperty =
                DependencyProperty.Register(
                    "LayoutMode",
                    typeof(LayoutMode),
                    typeof(GridRow),
                    new FrameworkPropertyMetadata(LayoutMode.Relative, OnLayoutModePropertyChanged));

        /// <summary>
        ///     The Column attached property.
        /// </summary>
        /// <remarks>
        ///     Applied to a child element, this value overrides the calculated Grid.Column attached property for the child.
        /// </remarks>
        public static readonly DependencyProperty ColumnProperty =
                DependencyProperty.RegisterAttached(
                    "Column",
                    typeof(int?),
                    typeof(GridRow),
                    new FrameworkPropertyMetadata(null, OnColumnPropertyChanged));

        /// <summary>
        ///     The GridRow attached property.
        /// </summary>
        /// <remarks>
        ///     This property is used internally to keep track of virtual children.
        /// </remarks>
        public static readonly DependencyProperty GridRowProperty =
                DependencyProperty.RegisterAttached(
                    "GridRow",
                    typeof(GridRow),
                    typeof(GridRow),
                    new FrameworkPropertyMetadata(null));

        #endregion

        #region Tracking fields

        /// <summary>
        ///     Keeps track of the previous state of the virtual children.
        /// </summary>
        /// <remarks>
        ///     This field is used to simplify collection change tracking.
        /// </remarks>
        private List<UIElement> previousState = new List<UIElement>();

        /// <summary>
        ///     Determines if the Grid.Column attached property event handler for items is active.
        /// </summary>
        private bool ignoreItemColumnChange;

        /// <summary>
        ///     Determines if the Grid.Row attached property event handler for items is active.
        /// </summary>
        private bool ignoreItemRowChange;

        #endregion

        #region Constructor

        /// <summary>
        ///     Initializes a new instance of the <see cref="GridRow"/> class.
        /// </summary>
        public GridRow() {
            var rowPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(Grid.RowProperty, typeof(GridRow));
            rowPropertyDescriptor.AddValueChanged(this, GridRow.OnAttachedPropertyChanged);

            var rowSpanPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(Grid.RowSpanProperty, typeof(GridRow));
            rowSpanPropertyDescriptor.AddValueChanged(this, GridRow.OnAttachedPropertyChanged);

            this.VirtualChildren = new ObservableCollection<UIElement>();
            this.VirtualChildren.CollectionChanged += this.ChildrenCollectionChanged;
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets or sets the layout mode.
        /// </summary>
        public LayoutMode LayoutMode {
            get { return (LayoutMode)this.GetValue(LayoutModeProperty); }
            set { this.SetValue(LayoutModeProperty, value); }
        }

        /// <summary>
        ///     Gets or sets the column of the first child.
        /// </summary>
        public int? StartColumn {
            get { return (int?)this.GetValue(StartColumnProperty); }
            set { this.SetValue(StartColumnProperty, value); }
        }

        /// <summary>
        ///     Gets the virtual children grouped in the row.
        /// </summary>
        public ObservableCollection<UIElement> VirtualChildren {
            get { return (ObservableCollection<UIElement>)this.GetValue(VirtualChildrenProperty); }
            private set { this.SetValue(VirtualChildrenProperty, value); }
        }

        /// <summary>
        ///     Gets the value of the Column attached property.
        /// </summary>
        /// <param name="element">The element from which to read the property value.</param>
        /// <returns>The value of the attached property.</returns>
        public static int? GetColumn(UIElement element) {
            if (element == null) {
                throw new ArgumentNullException("element");
            }

            return (int)element.GetValue(ColumnProperty);
        }

        /// <summary>
        ///     Sets the value of the Column attached property.
        /// </summary>
        /// <param name="element">The element to which to set the property value.</param>
        /// <param name="value">The value.</param>
        public static void SetColumn(UIElement element, int? value) {
            if (element == null) {
                throw new ArgumentNullException("element");
            }

            element.SetValue(ColumnProperty, value);
        }

        #endregion

        #region PropertyChange event handlers

        /// <summary>
        ///     Handles changed to the StartColumn property.
        /// </summary>
        /// <param name="d">The GridRow instance.</param>
        /// <param name="e">The parameter is not used.</param>
        private static void OnStartColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            GridRow.ResetColumns(d as GridRow);
        }

        /// <summary>
        ///     Handles property change events for the LayoutMode property.
        /// </summary>
        /// <param name="d">The GridRow instance.</param>
        /// <param name="e">The parameter is not used.</param>
        private static void OnLayoutModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            GridRow.OnAttachedPropertyChanged(d, null);
        }

        /// <summary>
        ///     Handles changes to the Column attached property for a GridRow child.
        /// </summary>
        /// <param name="d">The GridRow child for which the attached property has changed.</param>
        /// <param name="e">The parameter is not used.</param>
        private static void OnColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            GridRow.ResetColumns(d.GetValue(GridRowProperty) as GridRow);
        }

        /// <summary>
        ///     Handles property change events for properties attached to a GridRow instance.
        /// </summary>
        /// <param name="sender">The GridRow instance.</param>
        /// <param name="e">The parameter is not used.</param>
        private static void OnAttachedPropertyChanged(object sender, EventArgs e) {
            var gridRow = sender as GridRow;

            if (gridRow == null) {
                return;
            }

            GridRow.ResetRows(gridRow.Parent as Grid);
        }

        /// <summary>
        ///     Handles changes to the Grid.Column attached property for a GridRow child.
        /// </summary>
        /// <param name="sender">The GridRow child for which the attached property has changed.</param>
        /// <param name="e">The parameter is not used.</param>
        /// <remarks>
        ///     This handler reverts changes to the Grid.Column attached property from outside the GridRow.
        /// </remarks>
        private static void OnItemGridColumnPropertyChanged(object sender, EventArgs e) {
            var item = sender as UIElement;

            if (item == null) {
                return;
            }

            var gridRow = item.GetValue(GridRow.GridRowProperty) as GridRow;
            
            if (gridRow == null || gridRow.ignoreItemColumnChange) {
                return;
            }

            GridRow.ResetColumns(gridRow);
        }

        /// <summary>
        ///     Handles changes to the Grid.Row attached property for a GridRow child.
        /// </summary>
        /// <param name="sender">The GridRow child for which the attached property has changed.</param>
        /// <param name="e">The parameter is not used.</param>
        /// <remarks>
        ///     This handler reverts changes to the Grid.Row attached property from outside the GridRow.
        /// </remarks>
        private static void OnItemGridRowPropertyChanged(object sender, EventArgs e) {
            var item = sender as UIElement;

            if (item == null) {
                return;
            }

            var gridRow = item.GetValue(GridRow.GridRowProperty) as GridRow;

            if (gridRow == null || gridRow.ignoreItemRowChange) {
                return;
            }

            GridRow.ResetItemRows(gridRow, GridRow.DetermineRow(gridRow));
        }

        /// <summary>
        ///     Handles changes to the Grid.ColumnSpan attached property for a GridRow child.
        /// </summary>
        /// <param name="sender">The GridRow child for which the attached property has changed.</param>
        /// <param name="e">The parameter is not used.</param>
        private static void OnItemGridColumnSpanChanged(object sender, EventArgs e) {
            GridRow.ResetColumns(sender as GridRow);
        }

        #endregion

        #region Layouting wizardry

        private static void ResetColumns(GridRow row) {
            if (row == null) {
                return;
            }

            var col = row.StartColumn ?? 0;

            foreach (var child in row.VirtualChildren) {
                if (child == null) {
                    continue;
                }

                var childColumn = (int?)child.GetValue(ColumnProperty);

                if (childColumn != null) {
                    col = childColumn.Value;
                }

                row.ignoreItemColumnChange = true;
                Grid.SetColumn(child, col);
                row.ignoreItemColumnChange = false;

                col += Grid.GetColumnSpan(child);
            }
        }

        private static void ResetRows(Grid parent) {
            if (parent == null) {
                return;
            }

            var row = 0;

            foreach (var child in parent.Children.OfType<GridRow>()) {
                if (child.LayoutMode == LayoutMode.Absolute) {
                    row = Grid.GetRow(child);
                }

                GridRow.ResetItemRows(child, row);
                row += Grid.GetRowSpan(child);
            }
        }

        private static void ResetItemRows(GridRow @this, int row) {
            if (@this == null) {
                return;
            }

            var rowSpan = Grid.GetRowSpan(@this);

            @this.ignoreItemRowChange = true;

            foreach (var child in @this.VirtualChildren) {
                Grid.SetRow(child, row);
                Grid.SetRowSpan(child, rowSpan);
            }

            @this.ignoreItemRowChange = false;
        }

        private static int DetermineRow(GridRow gridRow) {
            var parent = gridRow.Parent as Grid;
            if (parent == null) {
                return 0;
            }

            if (gridRow.LayoutMode == LayoutMode.Absolute) {
                return Grid.GetRow(gridRow);
            }

            var row = 0;

            foreach (var child in parent.Children.OfType<GridRow>().TakeWhile(ch => ch != gridRow)) {
                if (child.LayoutMode == LayoutMode.Absolute) {
                    row = Grid.GetRow(child) + Grid.GetRowSpan(child);
                } else {
                    row += Grid.GetRowSpan(child);
                }
            }

            return row;
        }

        #endregion

        #region Cuckoo synchronization

        private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
            var idx = 0;
            var cnt = Math.Min(this.previousState.Count, this.VirtualChildren.Count);

            for (; idx < cnt; idx++) {
                if (this.previousState[idx] != this.VirtualChildren[idx]) {
                    break;
                }
            }

            var removed = this.previousState.Skip(idx).ToList();
            var added = this.VirtualChildren.Skip(idx).ToList();

            this.previousState = this.VirtualChildren.ToList();

            this.SyncChildren(removed, added);
        }

        private void SyncChildren(IEnumerable<UIElement> removed, IEnumerable<UIElement> added) {
            var parent = this.Parent as Grid;

            if (parent == null) {
                return;
            }

            foreach (var item in removed) {
                item.SetValue(GridRowProperty, null);

                this.DetachGridPropertyChangedEventHandlers(item);

                parent.Children.Remove(item);    
            }

            foreach (var item in added) {
                item.SetValue(GridRowProperty, this);

                this.VirtualChildren.CollectionChanged -= this.ChildrenCollectionChanged;
                this.VirtualChildren.Remove(item);

                if (item is GridColumn) {
                    throw new Exception("Nesting a GridColumn in a GridRow is not supported.");
                }

                parent.Children.Add(item);

                this.AttachGridPropertyChangedEventHandlers(item);

                this.VirtualChildren.Add(item);
                this.VirtualChildren.CollectionChanged += this.ChildrenCollectionChanged;
            }

            ResetItemRows(this, GridRow.DetermineRow(this));
            ResetColumns(this);
        }

        private void AttachGridPropertyChangedEventHandlers(UIElement item) {
            DependencyPropertyDescriptor.FromProperty(Grid.RowProperty, typeof(UIElement))
                .AddValueChanged(item, GridRow.OnItemGridRowPropertyChanged);

            DependencyPropertyDescriptor.FromProperty(Grid.ColumnProperty, typeof(UIElement))
                .AddValueChanged(item, GridRow.OnItemGridColumnPropertyChanged);

            DependencyPropertyDescriptor.FromProperty(Grid.ColumnSpanProperty, typeof(UIElement))
                .AddValueChanged(item, GridRow.OnItemGridColumnSpanChanged);
        }

        private void DetachGridPropertyChangedEventHandlers(UIElement item) {
            DependencyPropertyDescriptor.FromProperty(Grid.RowProperty, typeof(UIElement))
                .RemoveValueChanged(item, GridRow.OnItemGridRowPropertyChanged);

            DependencyPropertyDescriptor.FromProperty(Grid.ColumnProperty, typeof(UIElement))
                .RemoveValueChanged(item, GridRow.OnItemGridColumnPropertyChanged);

            DependencyPropertyDescriptor.FromProperty(Grid.ColumnSpanProperty, typeof(UIElement))
                .RemoveValueChanged(item, GridRow.OnItemGridColumnSpanChanged);
        }

        #endregion
    }
}
